--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit edea3f0ab3019bfbe01a5331042f1a2cf20307aa
Parents : 08fd00b
Author : Mark Qvist <bc7291552be7a58f361522990465165c>
Signature : T66BB85Valid, signed by author
Date : 2026-07-19T14:35:26+02:00
Map optimizations
Changes
Diff
diff --git a/sbapp/mapview/view.py b/sbapp/mapview/view.py
index f0f5f552..d2cee7c0 100644
--- a/sbapp/mapview/view.py
+++ b/sbapp/mapview/view.py
@@ -12,14 +12,7 @@ from kivy.graphics import Canvas, Color, Rectangle
from kivy.graphics.transformation import Matrix
from kivy.lang import Builder
from kivy.metrics import dp
-from kivy.properties import (
- AliasProperty,
- BooleanProperty,
- ListProperty,
- NumericProperty,
- ObjectProperty,
- StringProperty,
-)
+from kivy.properties import AliasProperty, BooleanProperty, ListProperty, NumericProperty, ObjectProperty, StringProperty
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.image import Image
from kivy.uix.label import Label
@@ -27,13 +20,7 @@ from kivy.uix.scatter import Scatter
from kivy.uix.widget import Widget
from mapview import Bbox, Coordinate
-from mapview.constants import (
- CACHE_DIR,
- MAX_LATITUDE,
- MAX_LONGITUDE,
- MIN_LATITUDE,
- MIN_LONGITUDE,
-)
+from mapview.constants import CACHE_DIR, MAX_LATITUDE, MAX_LONGITUDE, MIN_LATITUDE, MIN_LONGITUDE
from mapview.source import MapSource
from mapview.utils import clamp
@@ -103,12 +90,10 @@ Builder.load_string(
"""
)
-
class ClickableLabel(Label):
def on_ref_press(self, *args):
webbrowser.open(str(args[0]), new=2)
-
class Tile(Rectangle):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -117,19 +102,14 @@ class Tile(Rectangle):
@property
def cache_fn(self):
map_source = self.map_source
- fn = map_source.cache_fmt.format(
- image_ext=map_source.image_ext,
- cache_key=map_source.cache_key,
- **self.__dict__
- )
+ fn = map_source.cache_fmt.format( image_ext=map_source.image_ext, cache_key=map_source.cache_key, **self.__dict__)
return join(self.map_source.cache_dir, fn)
def set_source(self, cache_fn):
try:
self.source = cache_fn
self.state = "need-animation"
- except:
- pass
+ except: pass
class CustomMapMarker(ButtonBehavior, Image):
"""A marker on a map, that must be used on a :class:`MapMarker`
@@ -217,7 +197,6 @@ class MapMarker(ButtonBehavior, Image):
self._layer.remove_widget(self)
self._layer = None
-
class MapMarkerPopup(MapMarker):
is_open = BooleanProperty(False)
placeholder = ObjectProperty(None)
@@ -226,16 +205,12 @@ class MapMarkerPopup(MapMarker):
def add_widget(self, widget):
if not self.placeholder:
self.placeholder = widget
- if self.is_open:
- super().add_widget(self.placeholder)
- else:
- self.placeholder.add_widget(widget)
+ if self.is_open: super().add_widget(self.placeholder)
+ else: self.placeholder.add_widget(widget)
def remove_widget(self, widget):
- if widget is not self.placeholder:
- self.placeholder.remove_widget(widget)
- else:
- super().remove_widget(widget)
+ if widget is not self.placeholder: self.placeholder.remove_widget(widget)
+ else: super().remove_widget(widget)
def on_is_open(self, *args):
self.refresh_open_status()
@@ -244,10 +219,8 @@ class MapMarkerPopup(MapMarker):
self.is_open = not self.is_open
def refresh_open_status(self):
- if not self.is_open and self.placeholder.parent:
- super().remove_widget(self.placeholder)
- elif self.is_open and not self.placeholder.parent:
- super().add_widget(self.placeholder)
+ if not self.is_open and self.placeholder.parent: super().remove_widget(self.placeholder)
+ elif self.is_open and not self.placeholder.parent: super().add_widget(self.placeholder)
class MapLayer(Widget):
@@ -282,11 +255,8 @@ class MarkerMapLayer(MapLayer):
def insert_marker(self, marker, **kwargs):
if self.order_marker_by_latitude:
- before = list(
- takewhile(lambda i_m: i_m[1].lat < marker.lat, enumerate(self.children))
- )
- if before:
- kwargs['index'] = before[-1][0] + 1
+ before = list(takewhile(lambda i_m: i_m[1].lat < marker.lat, enumerate(self.children)))
+ if before: kwargs['index'] = before[-1][0] + 1
super().add_widget(marker, **kwargs)
@@ -297,13 +267,11 @@ class MarkerMapLayer(MapLayer):
def remove_widget(self, marker):
marker._layer = None
- if marker in self.markers:
- self.markers.remove(marker)
+ if marker in self.markers: self.markers.remove(marker)
super().remove_widget(marker)
def reposition(self):
- if not self.markers:
- return
+ if not self.markers: return
mapview = self.parent
set_marker_position = self.set_marker_position
bbox = None
@@ -320,10 +288,8 @@ class MarkerMapLayer(MapLayer):
# Store scatter offset on marker for apply in set_marker_position
marker._scatter_offset = scatter_offsets.get(marker, (0.0, 0.0))
set_marker_position(mapview, marker)
- if not marker.parent:
- self.insert_marker(marker)
- else:
- super().remove_widget(marker)
+ if not marker.parent: self.insert_marker(marker)
+ else: super().remove_widget(marker)
def set_marker_position(self, mapview, marker):
x, y = mapview.get_window_xy_from(marker.lat, marker.lon, mapview.zoom)
@@ -343,16 +309,13 @@ class MarkerMapLayer(MapLayer):
self.clear_widgets()
del self.markers[:]
-
class MapViewScatter(Scatter):
# internal
def on_transform(self, *args):
super().on_transform(*args)
self.parent.on_transform(self.transform)
- def collide_point(self, x, y):
- return True
-
+ def collide_point(self, x, y): return True
class MapView(Widget):
lon = NumericProperty()
@@ -536,10 +499,8 @@ class MapView(Widget):
coord = args[0]
lat = coord.lat
lon = coord.lon
- elif len(args) == 2:
- lat, lon = args
- else:
- raise Exception("Invalid argument for center_on")
+ elif len(args) == 2: lat, lon = args
+ else: raise Exception("Invalid argument for center_on")
lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE)
lat = clamp(lat, MIN_LATITUDE, MAX_LATITUDE)
scale = self._scatter.scale
@@ -563,8 +524,7 @@ class MapView(Widget):
float_zoom: If True (default), use calculated floating-point zoom level.
If False, use iterative search for integer zoom level only.
"""
- if not self.width or not self.height:
- return # Widget not sized yet
+ if not self.width or not self.height: return # Widget not sized yet
# Calculate center
lat_center = (lat_min + lat_max) / 2.0
@@ -590,15 +550,11 @@ class MapView(Widget):
pixel_height_0 = abs(y_max - y_min)
# Calculate zoom where each dimension fits
- if pixel_width_0 > 0:
- zoom_width = log2(view_width / pixel_width_0)
- else:
- zoom_width = max_zoom
+ if pixel_width_0 > 0: zoom_width = log2(view_width / pixel_width_0)
+ else: zoom_width = max_zoom
- if pixel_height_0 > 0:
- zoom_height = log2(view_height / pixel_height_0)
- else:
- zoom_height = max_zoom
+ if pixel_height_0 > 0: zoom_height = log2(view_height / pixel_height_0)
+ else: zoom_height = max_zoom
# Use the limiting dimension (smaller zoom = more zoomed out)
calculated_zoom = min(zoom_width, zoom_height)
@@ -630,43 +586,34 @@ class MapView(Widget):
"""Sets the zoom level, leaving the (x, y) at the exact same point
in the view.
"""
- zoom = clamp(
- zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom()
- )
+ zoom = clamp(zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom())
if int(zoom) == int(self._zoom):
- if scale is None:
- return
- elif scale == self.scale:
- return
+ if scale is None: return
+ elif scale == self.scale: return
scale = scale or 1.0
- # first, rescale the scatter
+ # First, rescale the scatter
scatter = self._scatter
scale = clamp(scale, scatter.scale_min, scatter.scale_max)
rescale = scale * 1.0 / scatter.scale
- scatter.apply_transform(
- Matrix().scale(rescale, rescale, rescale),
- post_multiply=True,
- anchor=scatter.to_local(x, y),
- )
+ scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y))
- # adjust position if the zoom changed
+ # Adjust position if the zoom changed
c1 = self.map_source.get_col_count(self._zoom)
c2 = self.map_source.get_col_count(zoom)
if c1 != c2:
f = float(c2) / float(c1)
self.delta_x = scatter.x + self.delta_x * f
self.delta_y = scatter.y + self.delta_y * f
- # back to 0 every time
+ # Back to 0 every time
scatter.apply_transform(Matrix().translate(-scatter.x, -scatter.y, 0), post_multiply=True)
- # avoid triggering zoom changes.
+ # Avoid triggering zoom changes.
self._zoom = zoom
self.zoom = self._zoom
def on_zoom(self, instance, zoom):
- if zoom == self._zoom:
- return
+ if zoom == self._zoom: return
x = self.map_source.get_x(zoom, self.lon) - self.delta_x
y = self.map_source.get_y(zoom, self.lat) - self.delta_y
self.set_zoom_at(zoom, x, y)
@@ -676,14 +623,10 @@ class MapView(Widget):
"""Return the current :class:`Coordinate` within the (x, y) widget
coordinate.
"""
- if zoom is None:
- zoom = self._zoom
+ if zoom is None: zoom = self._zoom
vx, vy = self.viewport_pos
scale = self._scale
- return Coordinate(
- lat=self.map_source.get_lat(zoom, y / scale + vy),
- lon=self.map_source.get_lon(zoom, x / scale + vx),
- )
+ return Coordinate(lat=self.map_source.get_lat(zoom, y / scale + vy), lon=self.map_source.get_lon(zoom, x / scale + vx))
def add_marker(self, marker, layer=None):
"""Add a marker into the layer. If layer is None, it will be added in
@@ -694,8 +637,7 @@ class MapView(Widget):
if not self._default_marker_layer:
layer = MarkerMapLayer()
self.add_layer(layer)
- else:
- layer = self._default_marker_layer
+ else: layer = self._default_marker_layer
layer.add_widget(marker)
layer.set_marker_position(self, marker)
@@ -714,14 +656,11 @@ class MapView(Widget):
Defaults to "window".
"""
assert mode in ("scatter", "window")
- if self._default_marker_layer is None and isinstance(layer, MarkerMapLayer):
- self._default_marker_layer = layer
+ if self._default_marker_layer is None and isinstance(layer, MarkerMapLayer): self._default_marker_layer = layer
self._layers.append(layer)
c = self.canvas
- if mode == "scatter":
- self.canvas = self.canvas_layers
- else:
- self.canvas = self.canvas_layers_out
+ if mode == "scatter": self.canvas = self.canvas_layers
+ else: self.canvas = self.canvas_layers_out
layer.canvas_parent = self.canvas
super().add_widget(layer)
self.canvas = c
@@ -738,15 +677,11 @@ class MapView(Widget):
def sync_to(self, other):
"""Reflect the lat/lon/zoom of the other MapView to the current one.
"""
- if self._zoom != other._zoom:
- self.set_zoom_at(other._zoom, *self.center)
+ if self._zoom != other._zoom: self.set_zoom_at(other._zoom, *self.center)
self.center_on(other.get_latlon_at(*self.center))
- # Private API
-
def __init__(self, **kwargs):
from kivy.base import EventLoop
-
EventLoop.ensure_window()
self._invalid_scale = True
self._tiles = []
@@ -763,8 +698,7 @@ class MapView(Widget):
with self._scatter.canvas:
self.canvas_map = Canvas()
self.canvas_layers = Canvas()
- with self.canvas:
- self.canvas_layers_out = Canvas()
+ with self.canvas: self.canvas_layers_out = Canvas()
self._scale_target_anim = False
self._scale_target = 1.0
self._touch_count = 0
@@ -775,7 +709,7 @@ class MapView(Widget):
super().__init__(**kwargs)
def _animate_color(self, dt):
- # fast path
+ # Fast path
d = self.animation_duration
if d == 0:
for tile in self._tiles:
@@ -789,33 +723,23 @@ class MapView(Widget):
else:
d = d / 1000.0
for tile in self._tiles:
- if tile.state != "need-animation":
- continue
+ if tile.state != "need-animation": continue
tile.g_color.a += dt / d
- if tile.g_color.a >= 1:
- tile.state = "animated"
+ if tile.g_color.a >= 1: tile.state = "animated"
for tile in self._tiles_bg:
- if tile.state != "need-animation":
- continue
+ if tile.state != "need-animation": continue
tile.g_color.a += dt / d
- if tile.g_color.a >= 1:
- tile.state = "animated"
+ if tile.g_color.a >= 1: tile.state = "animated"
def add_widget(self, widget):
- if isinstance(widget, MapMarker):
- self.add_marker(widget)
- elif isinstance(widget, MapLayer):
- self.add_layer(widget)
- else:
- super().add_widget(widget)
+ if isinstance(widget, MapMarker): self.add_marker(widget)
+ elif isinstance(widget, MapLayer): self.add_layer(widget)
+ else: super().add_widget(widget)
def remove_widget(self, widget):
- if isinstance(widget, MapMarker):
- self.remove_marker(widget)
- elif isinstance(widget, MapLayer):
- self.remove_layer(widget)
- else:
- super().remove_widget(widget)
+ if isinstance(widget, MapMarker): self.remove_marker(widget)
+ elif isinstance(widget, MapLayer): self.remove_layer(widget)
+ else: super().remove_widget(widget)
def on_map_relocated(self, zoom, coord):
pass
@@ -844,8 +768,7 @@ class MapView(Widget):
self.diff_scale_at(diff, *self._scale_target_pos, final = final)
# print(f"Scale = {self._scale} Target now = {self._scale_target} Diff = {diff}")
ret = self._scale_target != 0
- if not ret:
- self._pause = False
+ if not ret: self._pause = False
return ret
def diff_scale_at(self, d, x, y, final = False):
@@ -858,11 +781,7 @@ class MapView(Widget):
scatter = self._scatter
scale = clamp(scale, scatter.scale_min, scatter.scale_max)
rescale = scale * 1.0 / scatter.scale
- scatter.apply_transform(
- Matrix().scale(rescale, rescale, rescale),
- post_multiply=True,
- anchor=scatter.to_local(x, y),
- )
+ scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y))
# Create a final transform to always land on well-
# defined scaling factors
if final and self._allow_snap:
@@ -871,12 +790,7 @@ class MapView(Widget):
if int_diff < 0.08:
target = scatter.scale-diff
factor = target/scatter.scale
-
- scatter.apply_transform(
- Matrix().scale(factor, factor, factor),
- post_multiply=True,
- anchor=scatter.to_local(x, y),
- )
+ scatter.apply_transform(Matrix().scale(factor, factor, factor), post_multiply=True, anchor=scatter.to_local(x, y))
# print(f"Snapped scale. Self = {self._scale} Scale = {scale}")
else:
pass
@@ -903,7 +817,6 @@ class MapView(Widget):
next_scale = 2.0
dz = (next_scale/self._scale)-1.0
- # print(f"Diff zoom {self._scale} factor = {dz}")
self.animated_diff_scale_at(dz, *touch.pos)
return True
touch.grab(self)
@@ -930,10 +843,10 @@ class MapView(Widget):
def on_transform(self, *args):
self._invalid_scale = True
- if self._transform_lock:
- return
+ if self._transform_lock: return
self._transform_lock = True
- # recalculate viewport
+
+ # Recalculate viewport
map_source = self.map_source
zoom = self._zoom
scatter = self._scatter
@@ -970,7 +883,6 @@ class MapView(Widget):
zoom -= 1
scale *= 2.0
-
zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom)
if zoom != self._zoom:
self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale)
@@ -979,16 +891,14 @@ class MapView(Widget):
if zoom == map_source.min_zoom and round(scatter.scale, 2) < 1.0:
scatter.scale = 1.0
self.trigger_update(True)
- else:
- self.trigger_update(False)
+ else: self.trigger_update(False)
- if map_source.bounds:
- self._apply_bounds()
+ if map_source.bounds: self._apply_bounds()
self._transform_lock = False
self._scale = self._scatter.scale
def _apply_bounds(self):
- # if the map_source have any constraints, apply them here.
+ # If the map_source have any constraints, apply them here.
map_source = self.map_source
zoom = self._zoom
min_lon, min_lat, max_lon, max_lat = map_source.bounds
@@ -1003,17 +913,13 @@ class MapView(Widget):
oxmax, oymax = self._scatter.to_local(self.right, self.top)
s = self._scale
cxmin = oxmin - dx
- if cxmin < xmin:
- self._scatter.x += (cxmin - xmin) * s
+ if cxmin < xmin: self._scatter.x += (cxmin - xmin) * s
cymin = oymin - dy
- if cymin < ymin:
- self._scatter.y += (cymin - ymin) * s
+ if cymin < ymin: self._scatter.y += (cymin - ymin) * s
cxmax = oxmax - dx
- if cxmax > xmax:
- self._scatter.x -= (xmax - cxmax) * s
+ if cxmax > xmax: self._scatter.x -= (xmax - cxmax) * s
cymax = oymax - dy
- if cymax > ymax:
- self._scatter.y -= (ymax - cymax) * s
+ if cymax > ymax: self._scatter.y -= (ymax - cymax) * s
def on__pause(self, instance, value):
if not value:
@@ -1069,15 +975,7 @@ class MapView(Widget):
dirs = [0, 1, 0, -1, 0]
bbox_for_zoom = self.bbox_for_zoom
size = map_source.dp_tile_size
-
- (
- tile_x_first,
- tile_y_first,
- tile_x_last,
- tile_y_last,
- x_count,
- y_count,
- ) = bbox_for_zoom(vx, vy, self.width, self.height, zoom)
+ tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count = bbox_for_zoom(vx, vy, self.width, self.height, zoom)
# Adjust tiles behind us
for tile in self._tiles_bg[:]:
@@ -1087,21 +985,9 @@ class MapView(Widget):
f = 2 ** (zoom - tile.zoom)
w = self.width / f
h = self.height / f
- (
- btile_x_first,
- btile_y_first,
- btile_x_last,
- btile_y_last,
- _,
- _,
- ) = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom)
-
- if (
- tile_x < btile_x_first
- or tile_x >= btile_x_last
- or tile_y < btile_y_first
- or tile_y >= btile_y_last
- ):
+ btile_x_first, btile_y_first, btile_x_last, btile_y_last, _, _ = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom)
+
+ if tile_x < btile_x_first or tile_x >= btile_x_last or tile_y < btile_y_first or tile_y >= btile_y_last:
tile.state = "done"
self._tiles_bg.remove(tile)
self.canvas_map.before.remove(tile.g_color)
@@ -1117,12 +1003,7 @@ class MapView(Widget):
tile_x = tile.tile_x
tile_y = tile.tile_y
- if (
- tile_x < tile_x_first
- or tile_x >= tile_x_last
- or tile_y < tile_y_first
- or tile_y >= tile_y_last
- ):
+ if tile_x < tile_x_first or tile_x >= tile_x_last or tile_y < tile_y_first or tile_y >= tile_y_last:
tile.state = "done"
self.tile_map_set(tile_x, tile_y, False)
self._tiles.remove(tile)
@@ -1140,21 +1021,13 @@ class MapView(Widget):
turn = 0
while arm_size < arm_max:
for i in range(arm_size):
- if (
- not self.tile_in_tile_map(x, y)
- and y >= tile_y_first
- and y < tile_y_last
- and x >= tile_x_first
- and x < tile_x_last
- ):
+ if not self.tile_in_tile_map(x, y) and y >= tile_y_first and y < tile_y_last and x >= tile_x_first and x < tile_x_last:
self.load_tile(x, y, size, zoom)
x += dirs[turn % 4 + 1]
y += dirs[turn % 4]
- if turn % 2 == 1:
- arm_size += 1
-
+ if turn % 2 == 1: arm_size += 1
turn += 1
def load_tile(self, x, y, size, zoom):
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────